// 数据处理工作流编辑器 JavaScript - 第一部分
class WorkflowEditor {
    constructor() {
        this.nodes = new Map();
        this.connections = [];
        this.selectedNode = null;
        this.nodeIdCounter = 0;
        this.connectionIdCounter = 0;
        this.isDragging = false;
        this.isConnecting = false;
        this.tempConnection = null;
        this.canvas = null;
        this.svg = null;
        
        // 新增功能属性
        this.currentTheme = localStorage.getItem('workflow-theme') || 'light';

        this.canvasOffset = { x: 0, y: 0 };
        this.isPanning = false;
        this.panStart = { x: 0, y: 0 };
        this.dataFlowAnimations = new Map();
        this.isProcessing = false;
        
        // 画布拖动性能优化相关属性
        this.canvasPanAnimationId = null;
        
        this.init();
    }

    init() {
        this.canvas = document.getElementById('workflow-canvas');
        this.svg = document.getElementById('connections-svg');
        this.dataFlowLayer = document.querySelector('.data-flow-layer');
        this.setupEventListeners();
        this.createCanvasBoundary(); // 创建画布边界
        this.initTheme();

        this.updateStatus();
        
        // 测试拖拽功能
        setTimeout(() => {
            this.testDragAndDrop();
        }, 1000);
    }

    setupEventListeners() {
        // 工具栏按钮
        document.getElementById('theme-toggle').addEventListener('click', () => this.toggleTheme());
        document.getElementById('run-workflow').addEventListener('click', () => this.runWorkflow());
        document.getElementById('clear-workflow').addEventListener('click', () => {
            showPresetConfirm({
                title: '清空画布',
                description: '将删除当前所有节点与连接，操作不可撤销',
                onConfirm: () => this.clearWorkflow()
            });
        });
        document.getElementById('save-workflow').addEventListener('click', () => this.saveWorkflow());
        document.getElementById('load-workflow').addEventListener('click', () => this.loadWorkflow());
        
        
        // 日志面板
        document.getElementById('toggle-log').addEventListener('click', () => this.toggleLogPanel());
        document.getElementById('clear-log').addEventListener('click', () => this.clearLog());
        
        this.setupDragAndDrop();
        
        // 画布事件
        this.canvas.addEventListener('click', (e) => this.onCanvasClick(e));
        this.canvas.addEventListener('contextmenu', (e) => this.onCanvasRightClick(e));

        this.canvas.addEventListener('mousedown', (e) => this.onCanvasMouseDown(e));
        this.canvas.addEventListener('mousemove', (e) => this.onCanvasMouseMove(e));
        this.canvas.addEventListener('mouseup', (e) => this.onCanvasMouseUp(e));
        
        // 键盘事件
        document.addEventListener('keydown', (e) => this.onKeyDown(e));
        
        // 窗口大小改变时更新边界
        window.addEventListener('resize', () => {
            this.updateCanvasBoundary();
        });
        
        // 上下文菜单
        this.setupContextMenu();
        
        // 文件模态框
        this.setupFileModal();
    }

    // ==================== 主题切换功能 ====================
    initTheme() {
        document.documentElement.setAttribute('data-theme', this.currentTheme);
        this.updateThemeIcon();
    }
    
    toggleTheme() {
        this.currentTheme = this.currentTheme === 'light' ? 'dark' : 'light';
        document.documentElement.setAttribute('data-theme', this.currentTheme);
        localStorage.setItem('workflow-theme', this.currentTheme);
        this.updateThemeIcon();
        this.logMessage(`切换到${this.currentTheme === 'light' ? '浅色' : '暗黑'}主题`, 'info');
    }
    
    updateThemeIcon() {
        const themeIcon = document.querySelector('.theme-icon');
        if (themeIcon) {
            // 直接设置文本内容，避免重复
            themeIcon.textContent = this.currentTheme === 'light' ? '🌙' : '☀️';
        }
    }
    
    // ==================== 平移功能 ====================
    
    applyCanvasTransform() {
        const transform = `translate(${this.canvasOffset.x}px, ${this.canvasOffset.y}px)`;
        
        // 应用到所有非拖拽状态的节点（避免GPU操作干扰）
        this.nodes.forEach(node => {
            if (node.element && !node.element.classList.contains('dragging')) {
                node.element.style.transform = transform;
            }
        });
        
        // SVG连线层同步变换
        if (this.svg) {
            this.svg.style.transform = transform;
        }
        
        // 数据流动层
        if (this.dataFlowLayer) {
            this.dataFlowLayer.style.transform = transform;
        }
        
        // 画布边界
        if (this.canvasBoundary) {
            this.canvasBoundary.style.transform = transform;
        }
    }
    

    
    onCanvasMouseDown(e) {
        // 只有在点击画布背景或提示文本时才开始平移
        if ((e.target === this.canvas || e.target.closest('.drop-hint')) && !e.target.closest('.workflow-node')) {
            this.isPanning = true;
            this.panStart = { x: e.clientX - this.canvasOffset.x, y: e.clientY - this.canvasOffset.y };
            this.canvas.style.cursor = 'grabbing';
            e.preventDefault();
        }
    }
    
    onCanvasMouseMove(e) {
        if (this.isPanning) {
            // 直接更新坐标，避免重复的requestAnimationFrame调用
            this.canvasOffset.x = e.clientX - this.panStart.x;
            this.canvasOffset.y = e.clientY - this.panStart.y;
            
            // 使用单次requestAnimationFrame批量更新DOM
            if (!this.canvasPanAnimationId) {
                this.canvasPanAnimationId = requestAnimationFrame(() => {
                    this.applyCanvasTransform();
                    this.updateConnections();
                    this.canvasPanAnimationId = null;
                });
            }
        }
    }
    
    onCanvasMouseUp(e) {
        if (this.isPanning) {
            this.isPanning = false;
            this.canvas.style.cursor = 'grab';
            
            // 清理未完成的动画帧
            if (this.canvasPanAnimationId) {
                cancelAnimationFrame(this.canvasPanAnimationId);
                this.canvasPanAnimationId = null;
            }
        }
    }
    
    // ==================== 数据流动特效 ====================
    startDataFlow(connectionId) {
        const connection = this.connections.find(conn => conn.id === connectionId);
        if (!connection || !connection.element) return;
        
        // 添加连线动画效果
        connection.element.classList.add('active');
        
        // 创建数据流动粒子
        this.createDataParticles(connection);
        
        // 设置定时器清理
        setTimeout(() => {
            if (connection.element) {
                connection.element.classList.remove('active');
            }
        }, 3000);
    }
    
    createDataParticles(connection) {
        const fromNode = this.nodes.get(connection.from);
        const toNode = this.nodes.get(connection.to);
        if (!fromNode || !toNode) return;
        
        // 计算起始和结束位置（基于实际节点尺寸）
        const fromEl = fromNode.element;
        const toEl = toNode.element;
        const fromW = fromEl ? fromEl.offsetWidth : 150;
        const fromH = fromEl ? fromEl.offsetHeight : 75;
        const toH = toEl ? toEl.offsetHeight : 75;
        const startX = fromNode.x + fromW + 8; // 输出端口尖端
        const startY = fromNode.y + Math.round(fromH / 2);
        const endX = toNode.x - 8;             // 输入端口尖端
        const endY = toNode.y + Math.round(toH / 2);
        
        // 创建多个粒子
        for (let i = 0; i < 5; i++) {
            setTimeout(() => {
                this.createSingleParticle(startX, startY, endX, endY);
            }, i * 200);
        }
    }
    
    createSingleParticle(startX, startY, endX, endY) {
        const particle = document.createElement('div');
        particle.className = 'data-particle';
        particle.style.left = startX + 'px';
        particle.style.top = startY + 'px';
        
        if (this.isProcessing) {
            particle.classList.add('active');
        }
        
        this.dataFlowLayer.appendChild(particle);
        
        // 使用CSS动画移动粒子
        const deltaX = endX - startX;
        const deltaY = endY - startY;
        
        particle.style.transition = 'transform 2s cubic-bezier(0.25, 0.46, 0.45, 0.94)';
        requestAnimationFrame(() => {
            particle.style.transform = `translate(${deltaX}px, ${deltaY}px)`;
        });
        
        // 动画结束后清理
        setTimeout(() => {
            if (particle.parentNode) {
                particle.parentNode.removeChild(particle);
            }
        }, 2500);
    }
    
    startWorkflowAnimation() {
        this.isProcessing = true;
        
        // 按顺序激活连接
        this.connections.forEach((connection, index) => {
            setTimeout(() => {
                this.startDataFlow(connection.id);
            }, index * 1000);
        });
        
        // 添加节点处理效果
        this.nodes.forEach(node => {
            if (node.element) {
                node.element.classList.add('processing');
            }
        });
        
        // 结束动画
        setTimeout(() => {
            this.isProcessing = false;
            this.nodes.forEach(node => {
                if (node.element) {
                    node.element.classList.remove('processing');
                }
            });
        }, this.connections.length * 1000 + 3000);
    }

    // 测试拖拽功能
    testDragAndDrop() {
        console.log('=== 拖拽功能测试 ===');
        
        const operatorItems = document.querySelectorAll('.operator-item');
        console.log(`算子项数量: ${operatorItems.length}`);
        
        operatorItems.forEach((item, index) => {
            console.log(`算子 ${index + 1}:`);
            console.log(`  - 类型: ${item.dataset.type}`);
            console.log(`  - draggable: ${item.draggable}`);
            console.log(`  - 属性 draggable: ${item.getAttribute('draggable')}`);
        });
        
        console.log(`画布元素:`, this.canvas);
        console.log('=== 测试结束 ===');
    }

    setupDragAndDrop() {
        // 设置算子拖拽
        const operatorItems = document.querySelectorAll('.operator-item');
        console.log(`找到 ${operatorItems.length} 个算子项`);
        
        operatorItems.forEach((item, index) => {
            console.log(`设置算子 ${index + 1}: ${item.dataset.type}`);
            
            // 移除可能存在的重复事件监听器
            item.removeEventListener('dragstart', this.onOperatorDragStart.bind(this));
            item.addEventListener('dragstart', (e) => this.onOperatorDragStart(e));
            
            // 确保算子可以被拖拽
            item.draggable = true;
            item.setAttribute('draggable', 'true');
            
            // 添加拖拽结束事件监听
            item.addEventListener('dragend', (e) => {
                const operatorItem = e.target.closest('.operator-item');
                if (operatorItem) {
                    operatorItem.classList.remove('dragging-operator');
                }
                console.log('拖拽结束');
            });
            
            // 添加调试信息
            item.addEventListener('drag', (e) => {
                console.log('正在拖拽...');
            });
        });

        // 设置画布拖拽接收
        this.canvas.addEventListener('dragover', (e) => this.onCanvasDragOver(e));
        this.canvas.addEventListener('drop', (e) => this.onCanvasDrop(e));
        this.canvas.addEventListener('dragenter', (e) => this.onCanvasDragEnter(e));
        this.canvas.addEventListener('dragleave', (e) => this.onCanvasDragLeave(e));
        
        console.log('拖拽事件设置完成');
    }

    onOperatorDragStart(e) {
        console.log('拖拽开始');
        const operatorItem = e.target.closest('.operator-item');
        if (!operatorItem) {
            console.error('未找到算子项');
            return;
        }
        
        const operatorType = operatorItem.dataset.type;
        console.log(`拖拽算子类型: ${operatorType}`);
        
        if (!operatorType) {
            console.error('算子类型未定义');
            return;
        }
        
        // 设置拖拽数据
        e.dataTransfer.setData('text/plain', operatorType);
        e.dataTransfer.effectAllowed = 'copy';
        
        // 添加拖拽视觉反馈
        operatorItem.classList.add('dragging-operator');
        
        // 设置拖拽结束的清理事件
        const cleanup = () => {
            operatorItem.classList.remove('dragging-operator');
            document.removeEventListener('dragend', cleanup);
            console.log('清理拖拽状态');
        };
        document.addEventListener('dragend', cleanup);
        
        this.logMessage(`开始拖拽算子: ${operatorType}`, 'info');
    }

    onCanvasDragOver(e) {
        e.preventDefault();
        e.dataTransfer.dropEffect = 'copy';
    }

    onCanvasDragEnter(e) {
        e.preventDefault();
        // 只有在拖拽算子时才显示拖拽效果
        if (e.dataTransfer.types.includes('text/plain')) {
            this.canvas.classList.add('drag-over');
            console.log('进入画布拖拽区域');
        }
    }

    onCanvasDragLeave(e) {
        // 检查是否离开了画布区域（不仅仅是子元素）
        if (!this.canvas.contains(e.relatedTarget) && e.relatedTarget !== this.canvas) {
            this.canvas.classList.remove('drag-over');
            console.log('离开画布拖拽区域');
        }
    }

    onCanvasDrop(e) {
        e.preventDefault();
        this.canvas.classList.remove('drag-over');
        
        const operatorType = e.dataTransfer.getData('text/plain');
        if (!operatorType) {
            // 清理拖拽状态
            document.querySelectorAll('.dragging-operator').forEach(item => {
                item.classList.remove('dragging-operator');
            });
            return;
        }

        // 计算放置位置（使用与节点拖拽一致的坐标系统）
        const rect = this.canvas.getBoundingClientRect();
        const canvasX = e.clientX - rect.left;
        const canvasY = e.clientY - rect.top;
        
        // 直接使用画布相对坐标，并调整为算子框中心位置
        const nodeWidth = 150;
        const nodeHeight = 75;
        const x = canvasX - nodeWidth / 2;  // 让算子框中心对齐鼠标位置
        const y = canvasY - nodeHeight / 2;

        console.log(`放置位置: 鼠标位置(${canvasX}, ${canvasY}) -> 算子中心位置(${x}, ${y})`);

        // 创建新节点
        const newNode = this.createNode(operatorType, x, y);
        
        // 清理拖拽视觉反馈
        document.querySelectorAll('.dragging-operator').forEach(item => {
            item.classList.remove('dragging-operator');
        });
        
        if (newNode) {
            this.logMessage(`创建了新的${operatorType}节点`, 'success');
            // 立即应用画布变换到新节点
            this.applyCanvasTransform();
        } else {
            this.logMessage(`创建${operatorType}节点失败`, 'error');
        }
    }

    createNode(type, x, y) {
        console.log(`创建节点: type=${type}, x=${x}, y=${y}`);
        
        const nodeId = `node_${this.nodeIdCounter++}`;
        const nodeData = {
            id: nodeId,
            type: type,
            x: x,
            y: y,
            config: this.getDefaultConfig(type)
        };

        // 创建 DOM 元素
        const nodeElement = this.createNodeElement(nodeData);
        
        // 确保画布存在
        if (!this.canvas) {
            console.error('画布元素不存在');
            return null;
        }
        
        // 添加到画布
        this.canvas.appendChild(nodeElement);
        console.log(`节点元素已添加到画布:`, nodeElement);

        // 存储节点数据
        this.nodes.set(nodeId, nodeData);
        nodeData.element = nodeElement;

        // 设置节点事件
        this.setupNodeEvents(nodeElement, nodeData);
        
        // 隐藏提示文本
        this.hidePlaceholderText();
        
        // 更新状态
        this.updateStatus();
        
        console.log(`节点创建完成:`, nodeData);
        return nodeData;
    }
    
    hidePlaceholderText() {
        const dropHint = this.canvas.querySelector('.drop-hint');
        if (dropHint) {
            dropHint.style.display = 'none';
        }
    }

    createNodeElement(nodeData) {
        const node = document.createElement('div');
        node.className = `workflow-node ${nodeData.type}`;
        node.style.position = 'absolute';
        node.style.left = `${nodeData.x}px`;
        node.style.top = `${nodeData.y}px`;
        node.style.zIndex = '10';
        node.dataset.nodeId = nodeData.id;

        const icon = this.getNodeIcon(nodeData.type);
        const title = this.getNodeTitle(nodeData.type);

        node.innerHTML = `
            <div class="node-header">
                <div class="node-title">
                    <span class="node-icon">${icon}</span>
                    <span>${title}</span>
                </div>
                <button class="node-delete" title="删除节点">&times;</button>
            </div>
            <div class="node-content">
                ${this.getNodeDescription(nodeData.type)}
            </div>
            <div class="node-ports">
                ${this.createNodePorts(nodeData.type)}
            </div>
        `;

        console.log(`节点元素创建完成:`, {
            id: nodeData.id,
            type: nodeData.type,
            position: { x: nodeData.x, y: nodeData.y },
            element: node
        });

        return node;
    }

    getNodeIcon(type) {
        const icons = {
            'input': '📥',
            'output': '📤',
            'character-cleaner': '🔤',
            'sentence-cleaner': '📝'
        };
        return icons[type] || '🔧';
    }

    getNodeTitle(type) {
        const titles = {
            'input': '输入节点',
            'output': '输出节点',
            'character-cleaner': '字符清洗',
            'sentence-cleaner': '句子筛选'
        };
        return titles[type] || '未知节点';
    }

    getNodeDescription(type) {
        const descriptions = {
            'input': '数据输入源',
            'output': '处理结果输出',
            'character-cleaner': '字符级别清洗处理',
            'sentence-cleaner': '句子级别筛选过滤'
        };
        return descriptions[type] || '处理节点';
    }

    createNodePorts(type) {
        let ports = '';
        
        // 输入端口
        if (type !== 'input') {
            ports += '<div class="port input" data-port-type="input" title="输入端口"></div>';
        }
        
        // 输出端口
        if (type !== 'output') {
            ports += '<div class="port output" data-port-type="output" title="输出端口"></div>';
        }
        
        return ports;
    }

    getDefaultConfig(type) {
        const configs = {
            'input': {
                inputPath: '',
                format: 'pretrain',
                fileId: '',
                dataCount: 0
            },
            'output': {
                // 输出节点不需要任何配置，直接下载
            },
            'character-cleaner': {
                enable_invisible_chars_removal: false,
                enable_unicode_normalize: false,
                enable_traditional_convert: false,
                enable_html_removal: false,
                enable_markdown_removal: false,
                enable_decorative_symbols_removal: false,
                enable_personal_info_masking: false,
                enable_bank_card_masking: false,
                enable_network_identifiers_masking: false,
                enable_network_credentials_masking: false,
                enable_punctuation_normalize: false,
                enable_whitespace_removal: false
            },
            'sentence-cleaner': {
                enable_md5_dedup: false,
                enable_jieba_filter: false,
                enable_ngram_filter: false,
                enable_field_filter: false,
                enable_length_filter: false,
                repetition_threshold: 0.5,
                jieba_cut_all: false,
                ngram_similarity_threshold: 0.8,
                ngram_n: 2,
                target_fields: [],
                field_filter_mode: 'include',
                field_match_mode: 'any',
                field_case_sensitive: false,
                field_use_jieba: false,
                min_length: 1,
                max_length: 1000,
                length_count_mode: 'char'
            }
        };
        return configs[type] || {};
    }

    debugPortPositions(nodeElement, nodeData) {
        // 调试凰数：显示端口实际位置
        const ports = nodeElement.querySelectorAll('.port');
        ports.forEach(port => {
            const rect = port.getBoundingClientRect();
            const canvasRect = this.canvas.getBoundingClientRect();
            const portX = rect.left + rect.width/2 - canvasRect.left;
            const portY = rect.top + rect.height/2 - canvasRect.top;
            
            console.log(`Port ${port.className} at node ${nodeData.id}: x=${portX}, y=${portY}`);
            
            // 在端口位置添加红色调试点
            const debugDot = document.createElement('div');
            debugDot.className = 'debug-dot';
            debugDot.style.position = 'absolute';
            debugDot.style.left = `${portX - 2}px`;
            debugDot.style.top = `${portY - 2}px`;
            debugDot.style.width = '4px';
            debugDot.style.height = '4px';
            debugDot.style.background = 'red';
            debugDot.style.borderRadius = '50%';
            debugDot.style.zIndex = '1000';
            debugDot.style.pointerEvents = 'none';
            this.canvas.appendChild(debugDot);
        });
    }

    logMessage(message, type = 'info') {
        const logContent = document.getElementById('log-content');
        const timestamp = new Date().toLocaleTimeString();
        const logEntry = document.createElement('div');
        logEntry.className = `log-entry ${type}`;
        logEntry.textContent = `[${timestamp}] ${message}`;
        logContent.appendChild(logEntry);
        logContent.scrollTop = logContent.scrollHeight;
    }


    updateStatus() {
        document.getElementById('node-count').textContent = `节点: ${this.nodes.size}`;
        document.getElementById('connection-count').textContent = `连接: ${this.connections.length}`;
    }

    // 验证错误弹窗（精简显示）
    showValidationErrors(errors) {
        // 归一化为字符串并去重
        const items = Array.from(new Set((errors || []).map(e => String(e))));
        const escape = (s) => s
            .replace(/&/g, '&amp;')
            .replace(/</g, '&lt;')
            .replace(/>/g, '&gt;')
            .replace(/\"/g, '&quot;')
            .replace(/'/g, '&#39;');

        // 简洁显示：最多先展示6条，其余可展开
        const maxShow = 6;
        const head = items.slice(0, maxShow);
        const tail = items.slice(maxShow);

        // 高亮“输入格式相关”的错误
        const highlight = (msg) => /输入节点.*数据|格式|JSON/i.test(msg);
        const li = (msg) => `<li${highlight(msg) ? ' class="hl"' : ''}>${escape(msg)}</li>`;

        const listHeadHTML = `<ul>${head.map(li).join('')}</ul>`;
        const listTailHTML = tail.length ? `<details><summary>展开剩余 ${tail.length} 条</summary><ul>${tail.map(li).join('')}</ul></details>` : '';

        const bodyHTML = `
            <div class="validation-errors">
                <h3>验证失败</h3>
                <p>发现以下问题：</p>
                ${listHeadHTML}
                ${listTailHTML}
            </div>
        `;

        const modal = document.createElement('div');
        modal.className = 'modal';
        modal.innerHTML = `
            <div class="modal-content">
                <style>
                .validation-errors ul{margin:6px 0;padding-left:20px}
                .validation-errors li.hl{color:#c62828;font-weight:600}
                .modal-content details{margin-top:8px}
                </style>
                <div class="modal-header">
                    <h4>工作流验证错误</h4>
                    <button class="modal-close">&times;</button>
                </div>
                <div class="modal-body">${bodyHTML}</div>
                <div class="modal-footer">
                    <button class="btn btn-primary" onclick="this.closest('.modal').remove()">确认</button>
                </div>
            </div>
        `;

        document.body.appendChild(modal);
        modal.querySelector('.modal-close').addEventListener('click', () => modal.remove());
        modal.addEventListener('click', (e) => { if (e.target === modal) modal.remove(); });
    }



    clearWorkflow() {
        if (this.nodes.size === 0 && this.connections.length === 0) {
            this.logMessage('画布已经是空的', 'info');
            return;
        }
        
        // 使用自定义弹窗在调用处确认，这里直接执行清空逻辑
        this.nodes.forEach(nodeData => {
            if (nodeData.element) {
                nodeData.element.remove();
            }
        });
        
        this.connections.forEach(connection => {
            if (connection.element) {
                connection.element.remove();
            }
        });
        
        this.nodes.clear();
        this.connections = [];
        this.selectedNode = null;
        
        this.hideParameterPanel();
        this.showPlaceholderText();
        this.updateStatus();
        this.logMessage('工作流已清空', 'success');
    }
    
    showPlaceholderText() {
        const dropHint = this.canvas.querySelector('.drop-hint');
        if (dropHint) {
            dropHint.style.display = 'block';
        }
    }

    toggleLogPanel() {
        const logPanel = document.getElementById('log-panel');
        const toggleBtn = document.getElementById('toggle-log');
        
        if (logPanel.classList.contains('hidden')) {
            logPanel.classList.remove('hidden');
            toggleBtn.textContent = '隐藏日志';
        } else {
            logPanel.classList.add('hidden');
            toggleBtn.textContent = '显示日志';
        }
    }

    clearLog() {
        const logContent = document.getElementById('log-content');
        logContent.innerHTML = '<div class="log-entry">日志已清空</div>';
    }

    onCanvasClick(e) {
        if (e.target === this.canvas || e.target.closest('.drop-hint')) {
            this.selectedNode = null;
            document.querySelectorAll('.workflow-node.selected').forEach(node => {
                node.classList.remove('selected');
            });
            this.hideParameterPanel();
        }
    }

    onCanvasRightClick(e) {
        e.preventDefault();
    }

    onKeyDown(e) {
        if (e.key === 'Delete' && this.selectedNode) {
            this.deleteNode(this.selectedNode);
        }
        
        if (e.key === 'Escape') {
            this.onCanvasClick({ target: this.canvas });
        }
    }



    createCanvasBoundary() {
        // 创建画布边界元素
        this.canvasBoundary = document.createElement('div');
        this.canvasBoundary.className = 'canvas-boundary';
        this.canvas.appendChild(this.canvasBoundary);
        
        // 设置边界尺寸（根据画布大小计算）
        this.updateCanvasBoundary();
    }

    updateCanvasBoundary() {
        if (!this.canvasBoundary) return;
        
        const canvasRect = this.canvas.getBoundingClientRect();
        const padding = 25; // 边界内边距（从50px改为25px，使面积扩大一倍）
        
        // 边界区域尺寸（算子可放置区域）
        const boundaryWidth = canvasRect.width - padding * 2;
        const boundaryHeight = canvasRect.height - padding * 2;
        
        this.canvasBoundary.style.width = boundaryWidth + 'px';
        this.canvasBoundary.style.height = boundaryHeight + 'px';
        this.canvasBoundary.style.left = padding + 'px';
        this.canvasBoundary.style.top = padding + 'px';
    }

    setupContextMenu() {
        // 基础上下文菜单设置
    }

    setupFileModal() {
        // 基础文件模态框设置
    }

    hideParameterPanel() {
        const parameterContent = document.getElementById('parameter-content');
        parameterContent.innerHTML = '<div class="no-selection">请选择一个算子节点来配置参数</div>';
    }

    setupNodeEvents(nodeElement, nodeData) {
        // 删除按钮
        const deleteBtn = nodeElement.querySelector('.node-delete');
        deleteBtn.addEventListener('click', (e) => {
            e.stopPropagation();
            this.deleteNode(nodeData.id);
        });

        // 节点点击选择
        nodeElement.addEventListener('click', (e) => {
            e.stopPropagation();
            this.selectNode(nodeData.id);
        });

        // 节点拖拽
        this.setupNodeDragging(nodeElement, nodeData);

        // 端口连接
        this.setupPortEvents(nodeElement, nodeData);
        
        // 调试：在端口上显示位置信息
        if (window.DEBUG_PORTS) {
            this.debugPortPositions(nodeElement, nodeData);
        }
    }

    setupNodeDragging(nodeElement, nodeData) {
        let isDragging = false;
        let startX, startY, startLeft, startTop;
        let dragThreshold = 5; // 提高拖拽阈值，减少意外触发
        let hasMoved = false;
        let dragStartTime = 0;

        const onMouseDown = (e) => {
            // 忽略端口和删除按钮的点击
            if (e.target.closest('.port') || e.target.closest('.node-delete')) return;
            
            isDragging = true;
            hasMoved = false;
            dragStartTime = Date.now();
            startX = e.clientX;
            startY = e.clientY;
            startLeft = nodeData.x;
            startTop = nodeData.y;
            
            // 防止文本选中和默认拖拽行为
            e.preventDefault();
            e.stopPropagation();
            
            // 添加全局事件监听（优化性能，使用passive: false仅在必要时）
            document.addEventListener('mousemove', onMouseMove);
            document.addEventListener('mouseup', onMouseUp);
            
            // 预先设置样式，避免重复DOM操作
            nodeElement.style.zIndex = '1000';
            nodeElement.style.transition = 'none'; // 禁用过渡效果提高性能
        };

        const onMouseMove = (e) => {
            if (!isDragging) return;
            
            const deltaX = e.clientX - startX;
            const deltaY = e.clientY - startY;
            const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
            
            // 检查是否超过拖拽阈值
            if (!hasMoved && distance > dragThreshold) {
                hasMoved = true;
                nodeElement.classList.add('dragging');
                document.body.style.cursor = 'grabbing';
                document.body.style.userSelect = 'none';
                
                // 最小化视觉反馈，避免GPU操作
                nodeElement.style.opacity = '0.9';
            }
            
            if (hasMoved) {
                // 零延迟拖动：直接计算并设置最终位置
                const newX = startLeft + deltaX;
                const newY = startTop + deltaY;
                
                // 边界限制（简化计算）
                const canvasRect = this.canvas.getBoundingClientRect();
                const currentW = nodeElement.offsetWidth;
                const currentH = nodeElement.offsetHeight;
                nodeData.x = Math.max(0, Math.min(newX, canvasRect.width - currentW));
                nodeData.y = Math.max(0, Math.min(newY, canvasRect.height - currentH));
                
                // 直接设置位置，无任何变换
                nodeElement.style.left = `${nodeData.x}px`;
                nodeElement.style.top = `${nodeData.y}px`;
                
                // 立即更新连接线
                this.updateConnections();
            }
            
            e.preventDefault();
            e.stopPropagation();
        };

        const onMouseUp = (e) => {
            if (isDragging) {
                isDragging = false;
                
                // 最小化清理操作
                nodeElement.classList.remove('dragging');
                nodeElement.style.zIndex = '10';
                nodeElement.style.opacity = '';
                
                document.body.style.cursor = '';
                document.body.style.userSelect = '';
                
                document.removeEventListener('mousemove', onMouseMove);
                document.removeEventListener('mouseup', onMouseUp);
                
                // 重新应用画布变换（如果需要）
                this.applyCanvasTransform();
                
                // 点击检测
                const clickTime = Date.now() - dragStartTime;
                if (!hasMoved && clickTime < 200) {
                    this.selectNode(nodeData.id);
                }
            }
        };

        // 绑定鼠标按下事件
        nodeElement.addEventListener('mousedown', onMouseDown);
        
        // 防止右键菜单干扰拖拽
        nodeElement.addEventListener('contextmenu', (e) => {
            e.preventDefault();
        });
        
        // 添加触摸支持（移动设备）
        nodeElement.addEventListener('touchstart', (e) => {
            const touch = e.touches[0];
            const mouseEvent = new MouseEvent('mousedown', {
                clientX: touch.clientX,
                clientY: touch.clientY
            });
            onMouseDown(mouseEvent);
        }, { passive: false });
    }

    setupPortEvents(nodeElement, nodeData) {
        const ports = nodeElement.querySelectorAll('.port');
        
        ports.forEach(port => {
            port.addEventListener('mousedown', (e) => {
                e.stopPropagation();
                this.startConnection(nodeData.id, port.dataset.portType, e);
            });
        });
    }

    startConnection(nodeId, portType, event) {
        if (portType !== 'output') {
            const clickedPort = event && event.target && event.target.closest ? event.target.closest('.port') : null;
            if (clickedPort) {
                clickedPort.classList.add('forbidden');
            }
            this.canvas.style.cursor = 'not-allowed';
            setTimeout(() => {
                this.canvas.style.cursor = '';
                document.querySelectorAll('.port.forbidden').forEach(p => p.classList.remove('forbidden'));
            }, 300);
            return;
        }
        
        // 前端限制：每个输出端点只允许一条连接
        const hasOutgoing = this.connections.some(c => c.from === nodeId);
        if (hasOutgoing) {
            this.logMessage('该输出端点已连接一条线路，不能再连接', 'warning');
            return;
        }
        
        this.isConnecting = true;
        
        // 精确计算三角形端口的尖端位置
        const node = this.nodes.get(nodeId);
        const nodeEl = node.element;
        const nodeWidth = nodeEl ? nodeEl.offsetWidth : 150;
        const nodeHeight = nodeEl ? nodeEl.offsetHeight : 75;
        // 输出端口尖端：节点右边界 + 三角形宽度(8px)
        const portX = node.x + nodeWidth + 8;
        const portY = node.y + Math.round(nodeHeight / 2);
        
        this.tempConnection = {
            fromNode: nodeId,
            fromPort: portType,
            startX: portX,
            startY: portY
        };
        
        this.createTempConnectionLine(portX, portY);
        
        document.addEventListener('mousemove', this.onConnectionMouseMove.bind(this));
        document.addEventListener('mouseup', this.onConnectionMouseUp.bind(this));
        
        this.logMessage('开始创建连接...', 'info');
    }

    onConnectionMouseMove(e) {
        if (!this.isConnecting || !this.tempConnection) return;
        this.updateTempConnectionLine(e.clientX, e.clientY);

        const targetPort = e.target && e.target.closest ? e.target.closest('.port') : null;
        // 清理之前的 forbidden 状态
        document.querySelectorAll('.port.forbidden').forEach(p => p.classList.remove('forbidden'));
        
        if (targetPort) {
            const targetNode = targetPort.closest('.workflow-node');
            const targetType = targetPort.dataset.portType;
            const fromNodeId = this.tempConnection.fromNode;
            let invalid = false;
            
            if (!targetNode) {
                invalid = true;
            } else {
                const nodeId = targetNode.dataset.nodeId;
                // 仅允许 输出 -> 输入，其它组合（输出->输出 / 输入->输入）显示禁止
                if (targetType !== 'input') {
                    invalid = true;
                }
                // 禁止连接到自身节点
                else if (nodeId === fromNodeId) {
                    invalid = true;
                }
                // 输入端点已存在入线，禁止
                else if (this.connections.some(c => c.to === nodeId)) {
                    invalid = true;
                }
            }
            
            if (invalid) {
                targetPort.classList.add('forbidden');
                this.canvas.style.cursor = 'not-allowed';
            } else {
                this.canvas.style.cursor = '';
            }
        } else {
            this.canvas.style.cursor = '';
        }
    }

    onConnectionMouseUp(e) {
        if (!this.isConnecting || !this.tempConnection) return;
        
        const targetPort = e.target.closest('.port');
        const targetNode = e.target.closest('.workflow-node');
        
        if (targetPort && targetNode) {
            const targetType = targetPort.dataset.portType;
            const toId = targetNode.dataset.nodeId;
            const fromId = this.tempConnection.fromNode;
            
            // 只允许 输出 -> 输入
            if (targetType !== 'input') {
                this.logMessage('只允许从输出端点连接到输入端点', 'warning');
                this.endConnection();
                return;
            }
            
            // 禁止连接到自身节点
            if (toId === fromId) {
                this.logMessage('不能连接到自身节点', 'warning');
                this.endConnection();
                return;
            }
            
            // 前端限制：每个输入端点只允许一条连接
            const hasIncoming = this.connections.some(c => c.to === toId);
            if (hasIncoming) {
                this.logMessage('该输入端点已连接一条线路，不能再连接', 'warning');
                this.endConnection();
                return;
            }
            
            this.createConnection(fromId, toId);
        }
        
        this.endConnection();
    }

    createTempConnectionLine(x, y) {
        const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
        
        line.setAttribute('class', 'connection-line temp');
        line.setAttribute('x1', x);
        line.setAttribute('y1', y);
        line.setAttribute('x2', x);
        line.setAttribute('y2', y);
        line.setAttribute('id', 'temp-connection');
        
        this.svg.appendChild(line);
    }

    updateTempConnectionLine(x, y) {
        const tempLine = document.getElementById('temp-connection');
        if (!tempLine) return;
        
        // 将屏幕坐标转换为SVG坐标系（考虑画布偏移）
        const canvasRect = this.canvas.getBoundingClientRect();
        const svgX = x - canvasRect.left - this.canvasOffset.x;
        const svgY = y - canvasRect.top - this.canvasOffset.y;
        
        tempLine.setAttribute('x2', svgX);
        tempLine.setAttribute('y2', svgY);
    }

    endConnection() {
        this.isConnecting = false;
        this.tempConnection = null;
        
        const tempLine = document.getElementById('temp-connection');
        if (tempLine) {
            tempLine.remove();
        }
        
        // 清理禁止态和鼠标指针
        this.canvas.style.cursor = '';
        document.querySelectorAll('.port.forbidden').forEach(p => p.classList.remove('forbidden'));
        
        document.removeEventListener('mousemove', this.onConnectionMouseMove.bind(this));
        document.removeEventListener('mouseup', this.onConnectionMouseUp.bind(this));
    }

    createConnection(fromNodeId, toNodeId) {
        // 兜底：每个端点只允许一条连接
        if (this.connections.some(conn => conn.from === fromNodeId)) {
            this.logMessage('该输出端点已连接一条线路，不能再连接', 'warning');
            return null;
        }
        if (this.connections.some(conn => conn.to === toNodeId)) {
            this.logMessage('该输入端点已连接一条线路，不能再连接', 'warning');
            return null;
        }
        
        const existingConnection = this.connections.find(conn => 
            conn.from === fromNodeId && conn.to === toNodeId
        );
        
        if (existingConnection) {
            this.logMessage('连接已存在', 'warning');
            return null;
        }
        
        const connectionId = `conn_${this.connectionIdCounter++}`;
        const connection = {
            id: connectionId,
            from: fromNodeId,
            to: toNodeId
        };
        
        this.connections.push(connection);
        this.drawConnection(connection);
        this.updateStatus();
        
        this.logMessage(`创建连接: ${fromNodeId} → ${toNodeId}`, 'success');
        return connection;
    }

    drawConnection(connection) {
        const fromNode = this.nodes.get(connection.from);
        const toNode = this.nodes.get(connection.to);
        
        if (!fromNode || !toNode) return;
        
        const line = this.createConnectionLine(connection, fromNode, toNode);
        this.svg.appendChild(line);
        connection.element = line;
    }

    createConnectionLine(connection, fromNode, toNode) {
        const line = document.createElementNS('http://www.w3.org/2000/svg', 'path');
        
        // 精确计算三角形端口的尖端位置（基于实际节点尺寸）
        const fromEl = fromNode.element;
        const toEl = toNode.element;
        const fromWidth = fromEl ? fromEl.offsetWidth : 150;
        const fromHeight = fromEl ? fromEl.offsetHeight : 75;
        const toHeight = toEl ? toEl.offsetHeight : 75;
        // 输出端口尖端：节点右边界 + 三角形宽度(8px)
        const fromX = fromNode.x + fromWidth + 8;
        const fromY = fromNode.y + Math.round(fromHeight / 2);
        // 输入端口尖端：节点左边界 - 三角形宽度(8px)
        const toX = toNode.x - 8;
        const toY = toNode.y + Math.round(toHeight / 2);
        
        const controlOffset = Math.abs(toX - fromX) * 0.5;
        const path = `M ${fromX} ${fromY} C ${fromX + controlOffset} ${fromY} ${toX - controlOffset} ${toY} ${toX} ${toY}`;
        
        line.setAttribute('d', path);
        line.setAttribute('class', 'connection-line');
        line.setAttribute('data-connection-id', connection.id);
        
        return line;
    }

    updateConnections() {
        this.connections.forEach(connection => {
            const fromNode = this.nodes.get(connection.from);
            const toNode = this.nodes.get(connection.to);
            
            if (!fromNode || !toNode || !connection.element) return;
            
            // 精确计算三角形端口的尖端位置（基于实际节点尺寸）
            const fromEl = fromNode.element;
            const toEl = toNode.element;
            const fromWidth = fromEl ? fromEl.offsetWidth : 150;
            const fromHeight = fromEl ? fromEl.offsetHeight : 75;
            const toHeight = toEl ? toEl.offsetHeight : 75;
            // 输出端口尖端：节点右边界 + 三角形宽度(8px)
            const fromX = fromNode.x + fromWidth + 8;
            const fromY = fromNode.y + Math.round(fromHeight / 2);
            // 输入端口尖端：节点左边界 - 三角形宽度(8px)
            const toX = toNode.x - 8;
            const toY = toNode.y + Math.round(toHeight / 2);
            
            const controlOffset = Math.abs(toX - fromX) * 0.5;
            const path = `M ${fromX} ${fromY} C ${fromX + controlOffset} ${fromY} ${toX - controlOffset} ${toY} ${toX} ${toY}`;
            
            connection.element.setAttribute('d', path);
        });
    }

    selectNode(nodeId) {
        // 清除之前的选中状态
        this.selectedNode = null;
        document.querySelectorAll('.workflow-node.selected').forEach(node => {
            node.classList.remove('selected');
        });
        
        const nodeData = this.nodes.get(nodeId);
        if (nodeData) {
            this.selectedNode = nodeId;
            nodeData.element.classList.add('selected');
            this.showParameterPanel(nodeData);
            this.logMessage(`选中节点: ${this.getNodeTitle(nodeData.type)}`, 'info');
        }
    }

    deleteNode(nodeId) {
        const nodeData = this.nodes.get(nodeId);
        if (!nodeData) return;
        
        this.connections = this.connections.filter(connection => {
            if (connection.from === nodeId || connection.to === nodeId) {
                if (connection.element) {
                    connection.element.remove();
                }
                return false;
            }
            return true;
        });
        
        if (nodeData.element) {
            nodeData.element.remove();
        }
        
        this.nodes.delete(nodeId);
        
        if (this.selectedNode === nodeId) {
            this.selectedNode = null;
            this.hideParameterPanel();
        }
        
        this.updateStatus();
        this.logMessage(`删除节点: ${nodeId}`, 'info');
    }

    showParameterPanel(nodeData) {
        const parameterContent = document.getElementById('parameter-content');
        parameterContent.innerHTML = this.createParameterForm(nodeData);
        this.bindParameterEvents(nodeData);
    }

    createParameterForm(nodeData) {
        let formHTML = `<div class="parameter-form"><h4>${this.getNodeTitle(nodeData.type)}参数设置</h4>`;
        
        switch (nodeData.type) {
            case 'input':
                formHTML += this.createInputNodeForm(nodeData.config);
                break;
            case 'output':
                formHTML += this.createOutputNodeForm(nodeData.config);
                break;
            case 'character-cleaner':
                formHTML += this.createCharacterCleanerForm(nodeData.config);
                break;
            case 'sentence-cleaner':
                formHTML += this.createSentenceCleanerForm(nodeData.config);
                break;
        }
        
        formHTML += '</div>';
        return formHTML;
    }

    createInputNodeForm(config) {
        return `
            <div class="form-group">
                <label>输入文件</label>
                <div class="file-input-group">
                    <input type="text" id="inputPath" value="${config.inputPath || ''}" placeholder="点击选择文件" readonly>
                    <button type="button" class="btn btn-file" onclick="workflowEditor.selectInputFile()">选择文件</button>
                </div>
                ${config.fileId ? `<div class="file-info">文件ID: ${config.fileId}</div>` : ''}
            </div>
            <div class="form-group">
                <label>数据格式</label>
                <select id="format">
                    <option value="pretrain" ${config.format === 'pretrain' ? 'selected' : ''}>Pretrain</option>
                    <option value="sft" ${config.format === 'sft' ? 'selected' : ''}>SFT</option>
                    <option value="dpo" ${config.format === 'dpo' ? 'selected' : ''}>DPO</option>
                    <option value="kto" ${config.format === 'kto' ? 'selected' : ''}>KTO</option>
                </select>
            </div>
        `;
    }

    createOutputNodeForm(config) {
        return `
            <div class="form-group">
                <div class="file-info">
                    <p><strong>输出说明：</strong></p>
                    <p>• 工作流执行完成后，处理结果将自动下载</p>
                    <p>• 文件名格式：原文件名_processed.json</p>
                    <p>• 无需配置任何参数</p>
                </div>
            </div>
        `;
    }

    createCharacterCleanerForm(config) {
        const options = [
            { 
                key: 'enable_invisible_chars_removal', 
                label: '去除不可见字符',
                description: '移除零宽字符、BOM标记和控制字符，保留制表符、换行符等必要空白字符'
            },
            { 
                key: 'enable_unicode_normalize', 
                label: 'Unicode标准化',
                description: '将文本标准化为NFC格式，统一字符编码，解决组合字符显示不一致问题'
            },
            { 
                key: 'enable_traditional_convert', 
                label: '繁体转简体',
                description: '基于OpenCC的智能繁简转换，支持地区化词汇转换'
            },
            { 
                key: 'enable_html_removal', 
                label: '去除HTML标签',
                description: '基于BeautifulSoup的安全HTML解析，移除所有标签和属性，保留文本内容'
            },
            { 
                key: 'enable_markdown_removal', 
                label: '去除Markdown格式',
                description: '移除Markdown标题、链接、代码块、强调等格式标记，保留纯文本内容'
            },
            { 
                key: 'enable_decorative_symbols_removal', 
                label: '删除装饰符号',
                description: '删除表情符号、几何形状、音乐符号等装饰性符号，保留实用性符号'
            },
            { 
                key: 'enable_personal_info_masking', 
                label: '个人信息打码',
                description: '对手机号、邮箱、身份证号等个人敏感信息进行脱敏处理'
            },
            { 
                key: 'enable_bank_card_masking', 
                label: '银行卡号打码',
                description: '支持9种主流银行卡格式识别，保留前4位和后4位，中间用星号替代'
            },
            { 
                key: 'enable_network_identifiers_masking', 
                label: '网络标识符打码',
                description: '对MAC地址、IP地址（IPv4/IPv6）、子网掩码等网络标识符进行脱敏'
            },
            { 
                key: 'enable_network_credentials_masking', 
                label: '网络凭证打码',
                description: '对证书序列号、证书指纹、密码哈希等网络凭证信息进行安全处理'
            },
            { 
                key: 'enable_punctuation_normalize', 
                label: '标点符号转换',
                description: '将英文标点转为中文标点，统一引号格式，标准化省略号和破折号'
            },
            { 
                key: 'enable_whitespace_removal', 
                label: '删除空白字符',
                description: '删除多余的空格、制表符、换行符等空白字符，用于文本紧凑化处理'
            }
        ];
        
        let html = '<div class="checkbox-group">';
        options.forEach(option => {
            html += `
                <div class="checkbox-item" data-doc-key="${option.key}">
                    <div class="checkbox-item-header">
                        <label for="${option.key}">${option.label}</label>
                        <label class="switch">
                            <input type="checkbox" id="${option.key}" ${config[option.key] ? 'checked' : ''}>
                            <span class="slider"></span>
                        </label>
                    </div>
                    <div class="checkbox-item-description">
                        ${option.description}
                    </div>
                </div>
            `;
        });
        html += '</div>';
        
        return html;
    }

    createSentenceCleanerForm(config) {
        let html = '<div class="parameter-form">';
        
        // MD5去重配置
        html += `
            <div class="checkbox-item" data-doc-key="enable_md5_dedup" data-operator-type="sentence_cleaner">
                <div class="checkbox-item-header">
                    <label for="enable_md5_dedup">MD5去重</label>
                    <label class="switch">
                        <input type="checkbox" id="enable_md5_dedup" ${config.enable_md5_dedup ? 'checked' : ''}>
                        <span class="slider"></span>
                    </label>
                </div>
                <div class="checkbox-item-description">
                    计算句子或文本的MD5哈希值，用于快速去重，去除完全相同的文本内容
                </div>
            </div>
        `;
        
        // 长度过滤配置
        html += `
            <div class="checkbox-item" data-doc-key="enable_length_filter" data-operator-type="sentence_cleaner">
                <div class="checkbox-item-header">
                    <label for="enable_length_filter">长度过滤</label>
                    <label class="switch">
                        <input type="checkbox" id="enable_length_filter" ${config.enable_length_filter ? 'checked' : ''}>
                        <span class="slider"></span>
                    </label>
                </div>
                <div class="checkbox-item-description">
                    根据设定的最小和最大长度筛选文本，过滤过短或过长的内容
                </div>
                <div class="sub-params">
                    <div class="form-group" data-doc-key="min_length" data-operator-type="sentence_cleaner">
                        <label for="min_length">最小长度</label>
                        <input type="number" 
                               id="min_length" 
                               value="${config.min_length || 1}" 
                               min="1" 
                               max="1000">
                        <div class="checkbox-item-description">过滤掉小于此长度的文本，防止信息量不足的短文本干扰</div>
                    </div>
                    <div class="form-group" data-doc-key="max_length" data-operator-type="sentence_cleaner">
                        <label for="max_length">最大长度</label>
                        <input type="number" 
                               id="max_length" 
                               value="${config.max_length || 1000}" 
                               min="1" 
                               max="10000">
                        <div class="checkbox-item-description">过滤掉超过此长度的文本，控制过长文本的干扰和处理效率</div>
                    </div>
                    <div class="form-group" data-doc-key="length_count_mode" data-operator-type="sentence_cleaner">
                        <label for="length_count_mode">长度计算模式</label>
                        <select id="length_count_mode">
                            <option value="char" ${(config.length_count_mode || 'char') === 'char' ? 'selected' : ''}>字符计数</option>
                            <option value="word" ${(config.length_count_mode || 'char') === 'word' ? 'selected' : ''}>词数计数</option>
                        </select>
                        <div class="checkbox-item-description">选择按字符数还是词数来计算文本长度</div>
                    </div>
                </div>
            </div>
        `;
        
        // jieba重复度过滤配置
        html += `
            <div class="checkbox-item" data-doc-key="enable_jieba_filter" data-operator-type="sentence_cleaner">
                <div class="checkbox-item-header">
                    <label for="enable_jieba_filter">jieba重复度过滤</label>
                    <label class="switch">
                        <input type="checkbox" id="enable_jieba_filter" ${config.enable_jieba_filter ? 'checked' : ''}>
                        <span class="slider"></span>
                    </label>
                </div>
                <div class="checkbox-item-description">
                    基于jieba分词的语义重复检测，智能识别近似重复内容，去除词汇重复率过高的文本
                </div>
                <div class="sub-params">
                    <div class="form-group" data-doc-key="repetition_threshold" data-operator-type="sentence_cleaner">
                        <label for="repetition_threshold">重复率阈值</label>
                        <input type="number" 
                               id="repetition_threshold" 
                               value="${config.repetition_threshold || 0.3}" 
                               min="0" 
                               max="1" 
                               step="0.1">
                        <div class="checkbox-item-description">超过此重复率的文本将被过滤，计算方式：(总词数-唯一词数)/总词数</div>
                    </div>
                    <div class="form-group" data-doc-key="jieba_cut_all" data-operator-type="sentence_cleaner">
                        <label for="jieba_cut_all">使用全模式分词</label>
                        <label class="switch">
                            <input type="checkbox" id="jieba_cut_all" ${config.jieba_cut_all ? 'checked' : ''}>
                            <span class="slider"></span>
                        </label>
                        <div class="checkbox-item-description">启用jieba全模式分词，会产生更多分词结果，提高召回率</div>
                    </div>
                </div>
            </div>
        `;
        
        // N-gram相似度过滤配置
        html += `
            <div class="checkbox-item" data-doc-key="enable_ngram_filter" data-operator-type="sentence_cleaner">
                <div class="checkbox-item-header">
                    <label for="enable_ngram_filter">N-gram相似度过滤</label>
                    <label class="switch">
                        <input type="checkbox" id="enable_ngram_filter" ${config.enable_ngram_filter ? 'checked' : ''}>
                        <span class="slider"></span>
                    </label>
                </div>
                <div class="checkbox-item-description">
                    基于jieba分词的词级N-gram特征相似性检测，使用Jaccard相似度筛选相似文本
                </div>
                <div class="sub-params">
                    <div class="form-group" data-doc-key="ngram_similarity_threshold" data-operator-type="sentence_cleaner">
                        <label for="ngram_similarity_threshold">相似度阈值</label>
                        <input type="number" 
                               id="ngram_similarity_threshold" 
                               value="${config.ngram_similarity_threshold || 0.8}" 
                               min="0" 
                               max="1" 
                               step="0.1">
                        <div class="checkbox-item-description">超过此相似度的文本将被过滤，保持数据多样性和独特性</div>
                    </div>
                    <div class="form-group" data-doc-key="ngram_n" data-operator-type="sentence_cleaner">
                        <label for="ngram_n">N-gram大小</label>
                        <input type="number" 
                               id="ngram_n" 
                               value="${config.ngram_n || 2}" 
                               min="1" 
                               max="5">
                        <div class="checkbox-item-description">N-gram中N的大小，推荐使用2或3，值越大越精确但召回率降低</div>
                    </div>
                </div>
            </div>
        `;
        
        // 字段条件过滤配置
        html += `
            <div class="checkbox-item" data-doc-key="enable_field_filter" data-operator-type="sentence_cleaner">
                <div class="checkbox-item-header">
                    <label for="enable_field_filter">字段条件过滤</label>
                    <label class="switch">
                        <input type="checkbox" id="enable_field_filter" ${config.enable_field_filter ? 'checked' : ''}>
                        <span class="slider"></span>
                    </label>
                </div>
                <div class="checkbox-item-description">
                    根据指定字段的内容进行包含或排除过滤，可配置大小写敏感性和jieba分词匹配
                </div>
                <div class="sub-params">
                    <div class="form-group" data-doc-key="target_fields" data-operator-type="sentence_cleaner">
                        <label for="target_fields">目标关键词</label>
                        <input type="text" 
                               id="target_fields" 
                               value="${(config.target_fields || []).join(',')}"
                               placeholder="用逗号分隔多个关键词">
                        <div class="checkbox-item-description">目标字段列表，用逗号分隔，如：环保,人工智能,科技</div>
                    </div>
                    <div class="form-group" data-doc-key="field_filter_mode" data-operator-type="sentence_cleaner">
                        <label for="field_filter_mode">过滤模式</label>
                        <select id="field_filter_mode">
                            <option value="include" ${(config.field_filter_mode || 'include') === 'include' ? 'selected' : ''}>包含模式</option>
                            <option value="exclude" ${(config.field_filter_mode || 'include') === 'exclude' ? 'selected' : ''}>排除模式</option>
                        </select>
                        <div class="checkbox-item-description">包含模式：保留含有关键词的文本；排除模式：删除含有关键词的文本</div>
                    </div>
                    <div class="form-group" data-doc-key="field_match_mode" data-operator-type="sentence_cleaner">
                        <label for="field_match_mode">匹配模式</label>
                        <select id="field_match_mode">
                            <option value="any" ${(config.field_match_mode || 'any') === 'any' ? 'selected' : ''}>任一匹配</option>
                            <option value="all" ${(config.field_match_mode || 'any') === 'all' ? 'selected' : ''}>全部匹配</option>
                        </select>
                        <div class="checkbox-item-description">任一匹配：匹配任意一个关键词即可；全部匹配：必须同时匹配所有关键词</div>
                    </div>
                    <div class="form-group" data-doc-key="field_case_sensitive" data-operator-type="sentence_cleaner">
                        <label for="field_case_sensitive">大小写敏感</label>
                        <label class="switch">
                            <input type="checkbox" id="field_case_sensitive" ${config.field_case_sensitive ? 'checked' : ''}>
                            <span class="slider"></span>
                        </label>
                        <div class="checkbox-item-description">是否区分英文字母大小写进行匹配</div>
                    </div>
                    <div class="form-group" data-doc-key="field_use_jieba" data-operator-type="sentence_cleaner">
                        <label for="field_use_jieba">使用jieba分词</label>
                        <label class="switch">
                            <input type="checkbox" id="field_use_jieba" ${config.field_use_jieba ? 'checked' : ''}>
                            <span class="slider"></span>
                        </label>
                        <div class="checkbox-item-description">启用jieba分词进行语义匹配，提高中文关键词识别准确率</div>
                    </div>
                </div>
            </div>
        `;
        
        html += '</div>';
        
        return html;
    }

    bindParameterEvents(nodeData) {
        const parameterContent = document.getElementById('parameter-content');
        const inputs = parameterContent.querySelectorAll('input, select');
        
        inputs.forEach(input => {
            input.addEventListener('change', () => {
                let value;
                if (input.type === 'checkbox') {
                    value = input.checked;
                } else if (input.type === 'number') {
                    value = parseFloat(input.value) || 0;
                } else if (input.id === 'target_fields') {
                    // 处理逗号分隔的字段列表
                    value = input.value.split(',').map(s => s.trim()).filter(s => s.length > 0);
                } else {
                    value = input.value;
                }
                this.updateNodeConfig(nodeData, input.id, value);
            });
        });

        // 添加文档悬停事件监听
        const checkboxItems = parameterContent.querySelectorAll('.checkbox-item[data-doc-key]');
        checkboxItems.forEach(item => {
            const docKey = item.getAttribute('data-doc-key');
            
            item.addEventListener('mouseenter', () => {
                if (window.docTooltip) {
                    window.docTooltip.show(docKey, nodeData.type);
                }
            });
            
            item.addEventListener('mouseleave', () => {
                if (window.docTooltip) {
                    window.docTooltip.hide();
                }
            });
        });
    }

    updateNodeConfig(nodeData, key, value) {
        nodeData.config[key] = value;
        this.logMessage(`更新${nodeData.type}参数: ${key} = ${value}`, 'info');
    }

    selectInputFile() {
        const input = document.createElement('input');
        input.type = 'file';
        input.accept = '.json';
        input.onchange = async (e) => {
            const file = e.target.files[0];
            if (file) {
                try {
                    this.logMessage(`开始上传文件: ${file.name}`, 'info');
                    
                    // 上传文件（统一使用 multipart/form-data）
                    const formData = new FormData();
                    formData.append('file', file);
                    
                    let response = await fetch('/api/upload', {
                        method: 'POST',
                        body: formData
                    });
                    
                    let result;
                    try {
                        result = await response.json();
                    } catch (parseErr) {
                        // 返回非JSON（如错误页）时，作为失败处理
                        result = null;
                    }
                    
                    if (response.ok && result && result.success) {
                        // 更新输入路径显示
                        const inputPathElement = document.getElementById('inputPath');
                        if (inputPathElement) {
                            inputPathElement.value = `${file.name} (已上传, ${result.data_count} 条数据)`;
                        }
                        
                        // 更新节点配置
                        if (this.selectedNode) {
                            const nodeData = this.nodes.get(this.selectedNode);
                            if (nodeData) {
                                nodeData.config.inputPath = file.name;
                                nodeData.config.fileId = result.file_id;
                                nodeData.config.dataCount = result.data_count;
                                
                                // 重新显示参数面板
                                this.showParameterPanel(nodeData);
                            }
                        }
                        
                        this.logMessage(`文件上传成功: ${file.name}, 数据条数: ${result.data_count}`, 'success');
                    } else {
                        const errMsg = (result && result.error) ? result.error : `HTTP ${response.status}: ${response.statusText}`;
                        this.logMessage(`表单上传失败: ${errMsg}`, 'error');
                        if (result && result.install_hint) {
                            this.logMessage(`安装提示: ${result.install_hint}`, 'warning');
                        } else {
                            this.logMessage('请确认后端已安装 python-multipart 并重启服务器；离线环境可将实现放置到 offline_packages/python_multipart 或 offline_packages/multipart。', 'info');
                        }
                    }
                } catch (error) {
                    this.logMessage(`文件上传异常: ${error.message}`, 'error');
                }
            }
        };
        input.click();
    }



    runWorkflow() {
        if (this.nodes.size === 0) {
            this.logMessage('工作流为空，无法执行', 'warning');
            return;
        }
        
        this.logMessage('开始执行工作流...', 'info');
        document.getElementById('workflow-status').textContent = '状态: 执行中';
        document.getElementById('run-workflow').disabled = true;
        
        // 验证工作流
        this.validateWorkflow().then(validation => {
            if (!validation.valid) {
                this.logMessage(`工作流验证失败: ${validation.errors.join(', ')}`, 'error');
                // 弹窗显示验证错误
                this.showValidationErrors(validation.errors);
                document.getElementById('workflow-status').textContent = '状态: 验证失败';
                document.getElementById('run-workflow').disabled = false;
                return;
            }
            
            if (validation.warnings && validation.warnings.length > 0) {
                validation.warnings.forEach(warning => {
                    this.logMessage(`警告: ${warning}`, 'warning');
                });
            }
            
            // 执行工作流
            this.executeWorkflowAPI();
        }).catch(error => {
            this.logMessage(`工作流验证异常: ${error.message}`, 'error');
            // 弹窗显示异常信息
            this.showValidationErrors([`验证请求失败: ${error.message}`]);
            document.getElementById('workflow-status').textContent = '状态: 验证异常';
            document.getElementById('run-workflow').disabled = false;
        });
    }

    async validateWorkflow() {
        const workflowData = {
            nodes: Array.from(this.nodes.values()).map(node => ({
                id: node.id,
                type: node.type,
                config: node.config
            })),
            connections: this.connections.map(conn => ({
                id: conn.id,
                from: conn.from,
                to: conn.to
            }))
        };
        
        try {
            const response = await fetch('/api/workflow/validate', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json'
                },
                body: JSON.stringify(workflowData)
            });
            
            if (!response.ok) {
                throw new Error(`HTTP ${response.status}: ${response.statusText}`);
            }
            
            return await response.json();
        } catch (error) {
            throw new Error(`验证请求失败: ${error.message}`);
        }
    }

    async executeWorkflowAPI() {
        const workflowData = {
            nodes: Array.from(this.nodes.values()).map(node => ({
                id: node.id,
                type: node.type,
                x: node.x,
                y: node.y,
                config: node.config
            })),
            connections: this.connections.map(conn => ({
                id: conn.id,
                from: conn.from,
                to: conn.to
            }))
        };
        
        try {
            this.logMessage('正在调用后端API执行工作流...', 'info');
            
            const response = await fetch('/api/workflow/execute', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json'
                },
                body: JSON.stringify(workflowData)
            });
            
            const result = await response.json();
            
            if (result.success) {
                this.logMessage('工作流执行成功！', 'success');
                this.logMessage(`输入数据: ${result.results.input_count} 条`, 'info');
                this.logMessage(`输出数据: ${result.results.output_count} 条`, 'info');
                
                // 显示处理步骤
                result.results.processing_steps.forEach(step => {
                    this.logMessage(`${step.node_type} 处理完成: ${step.output_count} 条数据`, 'success');
                });
                
                // 自动下载输出文件
                if (result.results.output_file && result.results.output_file.download_url) {
                    this.logMessage(`正在下载输出文件: ${result.results.output_file.filename}`, 'info');
                    this.downloadFile(result.results.output_file.download_url, result.results.output_file.filename);
                } else if (result.results.output_file && result.results.output_file.error) {
                    this.logMessage(`输出文件错误: ${result.results.output_file.error}`, 'error');
                }
                
                document.getElementById('workflow-status').textContent = '状态: 执行成功';
                
                // 显示结果对话框
                this.showExecutionResults(result.results);
            } else {
                this.logMessage(`工作流执行失败: ${result.error}`, 'error');
                document.getElementById('workflow-status').textContent = '状态: 执行失败';
            }
        } catch (error) {
            this.logMessage(`工作流执行异常: ${error.message}`, 'error');
            document.getElementById('workflow-status').textContent = '状态: 执行异常';
        } finally {
            document.getElementById('run-workflow').disabled = false;
        }
    }

    downloadFile(url, filename) {
        try {
            const link = document.createElement('a');
            link.href = url;
            link.download = filename;
            link.style.display = 'none';
            document.body.appendChild(link);
            link.click();
            document.body.removeChild(link);
            
            this.logMessage(`文件下载已开始: ${filename}`, 'success');
        } catch (error) {
            this.logMessage(`文件下载失败: ${error.message}`, 'error');
        }
    }

    showExecutionResults(results) {
        const resultHTML = `
            <div class="execution-results">
                <h3>执行结果</h3>
                <div class="result-summary">
                    <p><strong>输入数据:</strong> ${results.input_count} 条</p>
                    <p><strong>输出数据:</strong> ${results.output_count} 条</p>
                    <p><strong>处理效率:</strong> ${((results.output_count / results.input_count) * 100).toFixed(1)}%</p>
                    ${results.output_file && results.output_file.filename ? 
                        `<p><strong>下载文件:</strong> ${results.output_file.filename}</p>` : 
                        '<p><strong>状态:</strong> 处理完成，数据已下载</p>'
                    }
                </div>
                <div class="processing-steps">
                    <h4>处理步骤:</h4>
                    <ul>
                        ${results.processing_steps.map(step => 
                            `<li>${step.node_type}: ${step.output_count} 条数据</li>`
                        ).join('')}
                    </ul>
                </div>
                <div class="output-preview">
                    <h4>输出数据预览:</h4>
                    <pre>${JSON.stringify(results.output_data.slice(0, 3), null, 2)}</pre>
                    ${results.output_data.length > 3 ? '<p>...（显示前3条）</p>' : ''}
                </div>
            </div>
        `;
        
        // 创建结果模态框
        const modal = document.createElement('div');
        modal.className = 'modal';
        modal.innerHTML = `
            <div class="modal-content">
                <div class="modal-header">
                    <h4>工作流执行结果</h4>
                    <button class="modal-close">&times;</button>
                </div>
                <div class="modal-body">
                    ${resultHTML}
                </div>
                <div class="modal-footer">
                    <button class="btn btn-primary" onclick="this.closest('.modal').remove()">确认</button>
                </div>
            </div>
        `;
        
        document.body.appendChild(modal);
        
        // 绑定关闭事件
        modal.querySelector('.modal-close').addEventListener('click', () => {
            modal.remove();
        });
        
        modal.addEventListener('click', (e) => {
            if (e.target === modal) {
                modal.remove();
            }
        });
    }

    saveWorkflow() {
        const workflow = {
            nodes: Array.from(this.nodes.values()),
            connections: this.connections.map(conn => ({
                id: conn.id,
                from: conn.from,
                to: conn.to
            }))
        };
        
        const dataStr = JSON.stringify(workflow, null, 2);
        const dataBlob = new Blob([dataStr], { type: 'application/json' });
        const url = URL.createObjectURL(dataBlob);
        
        const link = document.createElement('a');
        link.href = url;
        link.download = 'workflow.json';
        link.click();
        
        URL.revokeObjectURL(url);
        this.logMessage('工作流已保存', 'success');
    }

    loadWorkflow() {
        const input = document.createElement('input');
        input.type = 'file';
        input.accept = '.json';
        input.onchange = (e) => {
            const file = e.target.files[0];
            if (file) {
                const reader = new FileReader();
                reader.onload = (event) => {
                    try {
                        const workflow = JSON.parse(event.target.result);
                        this.loadWorkflowData(workflow);
                        this.logMessage('工作流已加载', 'success');
                    } catch (error) {
                        this.logMessage('工作流文件格式错误', 'error');
                    }
                };
                reader.readAsText(file);
            }
        };
        input.click();
    }

    loadWorkflowData(workflow) {
        this.clearWorkflow();
        
        // 加载节点
        workflow.nodes.forEach(nodeData => {
            const node = this.createNode(nodeData.type, nodeData.x, nodeData.y);
            node.config = nodeData.config;
        });
        
        // 加载连接
        workflow.connections.forEach(connData => {
            this.createConnection(connData.from, connData.to);
        });

        // 画布曾被拖动时，新的节点默认未应用transform，需要同步应用
        this.applyCanvasTransform();
        this.updateConnections();
    }

    // 预置工作流加载（带清空）
    loadPresetWorkflow(key) {
        this.clearWorkflow();
        let selectedNodeId = null;
        // 统一控制预置工作流的模块间距与起始位置
        const startX = 60;       // 起始X
        const startY = 140;      // 起始Y
        const gapX = 240;        // 模块之间的水平间距（左边缘到左边缘）。在150px宽度下约有90px的可见间隙
        const makeNode = (type, x, y, configUpdater) => {
            const node = this.createNode(type, x, y);
            if (typeof configUpdater === 'function') {
                node.config = { ...node.config };
                configUpdater(node.config);
            }
            return node;
        };
        if (key === 'basic_format_filter') {
            const input = makeNode('input', startX, startY);
            const cleaner = makeNode('character-cleaner', startX + gapX, startY, (c) => {
                c.enable_invisible_chars_removal = true;
                c.enable_unicode_normalize = true;
                c.enable_html_removal = true;
                c.enable_markdown_removal = true;
            });
            const output = makeNode('output', startX + gapX * 2, startY);
            this.createConnection(input.id, cleaner.id);
            this.createConnection(cleaner.id, output.id);
            selectedNodeId = cleaner.id;
        } else if (key === 'basic_format_filter_masking') {
            const input = makeNode('input', startX, startY);
            const cleaner = makeNode('character-cleaner', startX + gapX, startY, (c) => {
                c.enable_invisible_chars_removal = true;
                c.enable_unicode_normalize = true;
                c.enable_html_removal = true;
                c.enable_markdown_removal = true;
                c.enable_personal_info_masking = true;
                c.enable_bank_card_masking = true;
                c.enable_network_identifiers_masking = true;
                c.enable_network_credentials_masking = true;
            });
            const output = makeNode('output', startX + gapX * 2, startY);
            this.createConnection(input.id, cleaner.id);
            this.createConnection(cleaner.id, output.id);
            selectedNodeId = cleaner.id;
        } else if (key === 'basic_format_filter_masking_dedup') {
            const input = makeNode('input', startX, startY);
            const cleaner = makeNode('character-cleaner', startX + gapX, startY, (c) => {
                c.enable_invisible_chars_removal = true;
                c.enable_unicode_normalize = true;
                c.enable_html_removal = true;
                c.enable_markdown_removal = true;
                c.enable_personal_info_masking = true;
                c.enable_bank_card_masking = true;
                c.enable_network_identifiers_masking = true;
                c.enable_network_credentials_masking = true;
            });
            const dedup = makeNode('sentence-cleaner', startX + gapX * 2, startY, (c) => {
                c.enable_md5_dedup = true;
                c.enable_length_filter = true;
                c.min_length = 4;
                c.max_length = 1000;
            });
            const output = makeNode('output', startX + gapX * 3, startY);
            this.createConnection(input.id, cleaner.id);
            this.createConnection(cleaner.id, dedup.id);
            this.createConnection(dedup.id, output.id);
            selectedNodeId = dedup.id;
        } else if (key === 'advanced_sentence_quality') {
            const input = makeNode('input', startX, startY);
            const sentence = makeNode('sentence-cleaner', startX + gapX, startY, (c) => {
                c.enable_length_filter = true;
                c.min_length = 4;
                c.max_length = 1000;
                c.enable_md5_dedup = true;
            });
            const output = makeNode('output', startX + gapX * 2, startY);
            this.createConnection(input.id, sentence.id);
            this.createConnection(sentence.id, output.id);
            selectedNodeId = sentence.id;
        } else if (key === 'secure_formatting_pipeline') {
            const input = makeNode('input', startX, startY);
            const cleaner = makeNode('character-cleaner', startX + gapX, startY, (c) => {
                c.enable_invisible_chars_removal = true;
                c.enable_unicode_normalize = true;
                c.enable_html_removal = true;
                c.enable_markdown_removal = true;
                c.enable_punctuation_standardization = true; // 若支持标点标准化，可用此键
                c.enable_personal_info_masking = true;
                c.enable_bank_card_masking = true;
                c.enable_network_identifiers_masking = true;
                c.enable_network_credentials_masking = true;
            });
            const output = makeNode('output', startX + gapX * 2, startY);
            this.createConnection(input.id, cleaner.id);
            this.createConnection(cleaner.id, output.id);
            selectedNodeId = cleaner.id;
        } else if (key === 'nlp_ready_preprocess') {
            const input = makeNode('input', startX, startY);
            const cleaner = makeNode('character-cleaner', startX + gapX, startY, (c) => {
                c.enable_unicode_normalize = true;
                c.enable_markdown_removal = true;
                c.enable_html_removal = true;
                c.enable_traditional_simplified_convert = true; // 若支持繁简转换，可用此键
            });
            const sentence = makeNode('sentence-cleaner', startX + gapX * 2, startY, (c) => {
                c.enable_md5_dedup = true;
                c.enable_length_filter = true;
                c.min_length = 4;
                c.max_length = 1000;
            });
            const output = makeNode('output', startX + gapX * 3, startY);
            this.createConnection(input.id, cleaner.id);
            this.createConnection(cleaner.id, sentence.id);
            this.createConnection(sentence.id, output.id);
            selectedNodeId = sentence.id;
        } else {
            this.logMessage(`未知的预置工作流: ${key}`, 'warning');
            return;
        }
        // 同步应用当前画布偏移到新节点与连线，避免拖动后加载预置出现错位
        this.applyCanvasTransform();
        this.updateConnections();
        // 自动选中一个关键节点以显示参数面板
        if (selectedNodeId) {
            this.selectNode(selectedNodeId);
        }
        this.updateStatus();
        this.logMessage('预置工作流已加载', 'success');
    }
}

// 文档浮窗管理器
class DocumentationTooltip {
    constructor() {
        this.tooltip = document.getElementById('doc-tooltip');
        this.title = document.getElementById('doc-title');
        this.description = document.getElementById('doc-description');
        this.details = document.getElementById('doc-details');
        this.example = document.getElementById('doc-example');
        this.currentElement = null; // 记住当前悬停的元素
        
        this.setupEventListeners();
    }
    
    setupEventListeners() {
        
        // 点击外部关闭
        document.addEventListener('click', (e) => {
            if (!this.tooltip.contains(e.target) && !e.target.closest('.checkbox-item')) {
                this.hide();
            }
        });
        
        // ESC键关闭
        document.addEventListener('keydown', (e) => {
            if (e.key === 'Escape') {
                this.hide();
            }
        });
        
        // 绑定悬停事件
        this.bindHoverEvents();
    }
    
    bindHoverEvents() {
        // 使用事件委托来处理动态添加的元素
        document.addEventListener('mouseenter', (e) => {
            if (e.target && typeof e.target.closest === 'function') {
                const element = e.target.closest('[data-doc-key]');
                if (element) {
                    const docKey = element.getAttribute('data-doc-key');
                    const operatorType = element.getAttribute('data-operator-type') || 'character_cleaner';
                    this.show(docKey, operatorType, element);
                }
            }
        }, true);
        
        document.addEventListener('mouseleave', (e) => {
            if (e.target && typeof e.target.closest === 'function') {
                const element = e.target.closest('[data-doc-key]');
                if (element) {
                    this.hide();
                }
            }
        }, true);
    }
    
    show(parameterKey, operatorType = 'character_cleaner', currentElement = null) {
        // 记住当前悬停的元素
        this.currentElement = currentElement;
        
        const docData = window.DOCS_DATA[operatorType];
        if (!docData || !docData.parameters[parameterKey]) {
            console.warn(`未找到参数 ${parameterKey} 的文档`);
            return;
        }
        
        const paramDoc = docData.parameters[parameterKey];
        
        // 设置内容
        this.title.textContent = paramDoc.title;
        this.description.textContent = paramDoc.description;
        
        // 设置详细信息
        this.details.innerHTML = '';
        if (paramDoc.details && paramDoc.details.length > 0) {
            const detailsList = document.createElement('ul');
            detailsList.className = 'doc-details-list';
            paramDoc.details.forEach(detail => {
                const li = document.createElement('li');
                li.textContent = detail;
                detailsList.appendChild(li);
            });
            this.details.appendChild(detailsList);
        }
        
        // 设置示例
        this.example.innerHTML = '';
        if (paramDoc.example) {
            const exampleTitle = document.createElement('div');
            exampleTitle.className = 'doc-example-title';
            exampleTitle.textContent = '示例：';
            
            const exampleContent = document.createElement('div');
            exampleContent.className = 'doc-example-content';
            exampleContent.textContent = paramDoc.example;
            
            this.example.appendChild(exampleTitle);
            this.example.appendChild(exampleContent);
        }
        
        // 暂时禁用transition以确保样式立即应用
        this.tooltip.style.transition = 'none';
        
        // 显示浮窗
        this.tooltip.classList.remove('hidden');
        this.tooltip.classList.add('show');
        
        // 强制重绘
        this.tooltip.offsetHeight;
        
        // 恢复transition
        this.tooltip.style.transition = '';
        
        // 定位到左侧
        this.positionTooltip();
    }
    
    hide() {
        this.tooltip.classList.remove('show');
        this.tooltip.classList.add('hidden');
        this.currentElement = null; // 清除当前元素
    }
    
    positionTooltip() {
        if (!this.currentElement) {
            return;
        }
        
        // 获取悬停元素的位置
        const elementRect = this.currentElement.getBoundingClientRect();
        
        // 获取工作流画布的边界
        const canvas = document.querySelector('.workflow-canvas');
        const canvasRect = canvas ? canvas.getBoundingClientRect() : null;
        
        // 清除 CSS 中的 transform，以便我们可以精确控制位置
        this.tooltip.style.transform = 'none';
        
        // 获取浮窗的尺寸（在清除 transform 后）
        const tooltipRect = this.tooltip.getBoundingClientRect();
        
        // 改为上方居中定位：默认显示在元素上方居中，间距10px
        let left = elementRect.left + (elementRect.width - tooltipRect.width) / 2;
        let top = elementRect.top - tooltipRect.height - 10;
        
        // 如果上方空间不足，显示在下方居中
        if (canvasRect && top < canvasRect.top + 10) {
            top = elementRect.bottom + 10;
        }
        
        // 画布左右边界修正
        if (canvasRect) {
            if (left < canvasRect.left + 10) {
                left = canvasRect.left + 10;
            }
            if (left + tooltipRect.width > canvasRect.right - 10) {
                left = canvasRect.right - tooltipRect.width - 10;
            }
            // 上下边界修正
            if (top < canvasRect.top + 10) {
                top = canvasRect.top + 10;
            }
            if (top + tooltipRect.height > canvasRect.bottom - 10) {
                top = canvasRect.bottom - tooltipRect.height - 10;
            }
        }
        
        // 应用位置
        this.tooltip.style.left = `${left}px`;
        this.tooltip.style.top = `${top}px`;
    }
}

// 初始化编辑器
document.addEventListener('DOMContentLoaded', () => {
    window.workflowEditor = new WorkflowEditor();
    window.docTooltip = new DocumentationTooltip();
    
    // 绑定预置项点击事件（带确认弹窗）
    const presetItems = document.querySelectorAll('.preset-item');
    presetItems.forEach(item => {
        item.addEventListener('click', () => {
            const key = item.getAttribute('data-doc-key');
            const title = item.querySelector('.preset-title')?.textContent?.trim() || '预置工作流';
            const desc = item.querySelector('.preset-desc')?.textContent?.trim() || '';
            showPresetConfirm({
                title,
                description: desc,
                onConfirm: () => {
                    window.workflowEditor?.loadPresetWorkflow(key);
                }
            });
        });
    });
    
    // 添加全局测试函数
    window.testDrag = () => {
        console.log('手动测试拖拽功能...');
        if (window.workflowEditor) {
            window.workflowEditor.testDragAndDrop();
        }
    };
    
    // 添加调试快捷键（Ctrl+D）
    document.addEventListener('keydown', (e) => {
        if (e.ctrlKey && e.key === 'd') {
            e.preventDefault();
            window.DEBUG_PORTS = !window.DEBUG_PORTS;
            console.log(`端口调试模式: ${window.DEBUG_PORTS ? '开启' : '关闭'}`);
            if (window.DEBUG_PORTS) {
                console.log('请创建新节点来查看端口位置调试信息');
            } else {
                // 清除调试点
                document.querySelectorAll('.debug-dot').forEach(dot => dot.remove());
            }
        }
        
        // 添加拖拽测试快捷键（Ctrl+T）
        if (e.ctrlKey && e.key === 't') {
            e.preventDefault();
            window.testDrag();
        }
    });
});

// 简易确认弹窗（不依赖外部库）
function showPresetConfirm({ title, description, onConfirm }) {
    // 如果已存在，先移除
    const existing = document.getElementById('preset-confirm');
    if (existing) existing.remove();

    const modal = document.createElement('div');
    modal.id = 'preset-confirm';
    modal.style.position = 'fixed';
    modal.style.left = '0';
    modal.style.top = '0';
    modal.style.right = '0';
    modal.style.bottom = '0';
    modal.style.background = 'rgba(0,0,0,0.38)';
    modal.style.display = 'flex';
    modal.style.alignItems = 'center';
    modal.style.justifyContent = 'center';
    modal.style.zIndex = '10000';

    const panel = document.createElement('div');
    panel.style.width = '520px';
    panel.style.maxWidth = '86vw';
    panel.style.background = '#1f2937';
    panel.style.color = '#fff';
    panel.style.border = '1px solid #374151';
    panel.style.borderRadius = '12px';
    panel.style.boxShadow = '0 12px 36px rgba(0,0,0,0.45)';
    panel.style.padding = '20px 22px';

    const h3 = document.createElement('h3');
    h3.textContent = `加载预置：${title}`;
    h3.style.margin = '0 0 8px 0';
    h3.style.fontSize = '18px';
    h3.style.fontWeight = '600';

    const p = document.createElement('p');
    p.textContent = description;
    p.style.margin = '0 0 16px 0';
    p.style.fontSize = '14px';
    p.style.color = '#d1d5db';

    const hint = document.createElement('div');
    hint.textContent = '该操作将清空当前画布并加载预置工作流，是否继续？';
    hint.style.margin = '0 0 12px 0';
    hint.style.fontSize = '13px';
    hint.style.color = '#9ca3af';

    const btns = document.createElement('div');
    btns.style.display = 'flex';
    btns.style.justifyContent = 'flex-end';
    btns.style.gap = '12px';

    const cancel = document.createElement('button');
    cancel.textContent = '取消';
    cancel.style.padding = '8px 14px';
    cancel.style.borderRadius = '8px';
    cancel.style.border = '1px solid #6b7280';
    cancel.style.background = '#111827';
    cancel.style.color = '#e5e7eb';
    cancel.addEventListener('click', () => modal.remove());

    const confirm = document.createElement('button');
    confirm.textContent = '确定加载';
    confirm.setAttribute('data-role', 'confirm');
    confirm.style.padding = '8px 14px';
    confirm.style.borderRadius = '8px';
    confirm.style.border = '1px solid #2563eb';
    confirm.style.background = '#2563eb';
    confirm.style.color = '#fff';
    confirm.style.fontWeight = '600';
    confirm.addEventListener('click', () => {
        try {
            onConfirm && onConfirm();
        } finally {
            modal.remove();
        }
    });

    panel.appendChild(h3);
    panel.appendChild(p);
    panel.appendChild(hint);
    panel.appendChild(btns);
    btns.appendChild(cancel);
    btns.appendChild(confirm);
    modal.appendChild(panel);
    document.body.appendChild(modal);

    // ESC 键关闭
    const onKey = (e) => {
        if (e.key === 'Escape') {
            modal.remove();
            document.removeEventListener('keydown', onKey);
        }
    };
    document.addEventListener('keydown', onKey);
}
// === 面板大小可拖拽调整：算子库 / 画布 / 参数区 ===
(function(){
  function getInt(val, fallback){
    var n = parseInt(val, 10);
    return isNaN(n) ? fallback : n;
  }

  function setupPanelResizers(){
    var main = document.querySelector('.main-content');
    var leftPanel = document.querySelector('.operator-panel');
    var rightPanel = document.querySelector('.parameter-panel');
    var leftResizer = document.querySelector('.panel-resizer[data-resizer="left"]');
    var rightResizer = document.querySelector('.panel-resizer[data-resizer="right"]');
    if(!main || !leftPanel || !rightPanel || !leftResizer || !rightResizer) return;

    var canvasMin = 600; // 与样式保持一致，避免画布过窄

    var resizeScheduled = false;
    function scheduleResize(){
      if(resizeScheduled) return;
      resizeScheduled = true;
      try {
        (window.requestAnimationFrame || function(cb){ return setTimeout(cb, 16); })(function(){
          resizeScheduled = false;
          try { window.dispatchEvent(new Event('resize')); } catch(err){}
        });
      } catch(err){}
    }

    function startDrag(which, downEvent){
      downEvent.preventDefault();
      var rect = main.getBoundingClientRect();
      var resizersWidth = (leftResizer.offsetWidth || 0) + (rightResizer.offsetWidth || 0);
      var minLeft = getInt(getComputedStyle(leftPanel).minWidth, 220);
      var minRight = getInt(getComputedStyle(rightPanel).minWidth, 240);

      function onMove(e){
        if(which === 'left'){
          var newLeft = e.clientX - rect.left;
          var maxLeft = rect.width - minRight - resizersWidth - canvasMin;
          newLeft = Math.max(minLeft, Math.min(newLeft, maxLeft));
          leftPanel.style.width = newLeft + 'px';
        } else {
          var newRight = rect.right - e.clientX;
          var maxRight = rect.width - minLeft - resizersWidth - canvasMin;
          newRight = Math.max(minRight, Math.min(newRight, maxRight));
          rightPanel.style.width = newRight + 'px';
        }
        // 节流触发窗口 resize（每帧最多一次），避免拖动产生过多重计算
        scheduleResize();
      }

      function onUp(){
        document.removeEventListener('mousemove', onMove);
        document.removeEventListener('mouseup', onUp);
        try {
          var sizes = { left: leftPanel.offsetWidth, right: rightPanel.offsetWidth };
          localStorage.setItem('panelSizes', JSON.stringify(sizes));
        } catch(err){}
      }

      document.addEventListener('mousemove', onMove);
      document.addEventListener('mouseup', onUp);
    }

    leftResizer.addEventListener('mousedown', function(e){ startDrag('left', e); });
    rightResizer.addEventListener('mousedown', function(e){ startDrag('right', e); });
  }

  function restorePanelSizes(){
    try {
      var data = localStorage.getItem('panelSizes');
      if(!data) return;
      var sizes = JSON.parse(data);
      var leftPanel = document.querySelector('.operator-panel');
      var rightPanel = document.querySelector('.parameter-panel');
      if(leftPanel && sizes.left) leftPanel.style.width = sizes.left + 'px';
      if(rightPanel && sizes.right) rightPanel.style.width = sizes.right + 'px';
      // 恢复已保存的尺寸后，触发一次重计算
      scheduleResize();
    } catch(err){}
  }

  if(document.readyState === 'loading'){
    document.addEventListener('DOMContentLoaded', function(){
      setupPanelResizers();
      restorePanelSizes();
    });
  } else {
    setupPanelResizers();
    restorePanelSizes();
  }
})();